﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Security;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using vm.Aspects.Diagnostics.DumpImplementation;
using vm.Aspects.Properties;

namespace vm.Aspects.Diagnostics
{
    public sealed partial class ObjectTextDumper
    {
        #region Fields
        /// <summary>
        /// The dump writer.
        /// </summary>
        TextWriter _writer;

        /// <summary>
        /// The current indent.
        /// </summary>
        int _indentLevel;

        /// <summary>
        /// The number of spaces in a single indent.
        /// </summary>
        int _indentLength;

        /// <summary>
        /// The current maximum depth of recursing into the aggregated objects. When it goes down to 0 - the recursion should stop.
        /// </summary>
        int? _maxDepth;

        /// <summary>
        /// Contains references to all dumped objects to avoid infinite dumping due to cyclical references.
        /// </summary>
        HashSet<DumpedObject> _dumpedObjects = new HashSet<DumpedObject>();

        /// <summary>
        /// Contains references to all dumped virtual properties to avoid dumping them more than once in the derived classes.
        /// </summary>
        HashSet<DumpedProperty> _dumpedVirtualProperties = new HashSet<DumpedProperty>();
        #endregion

        #region Constructor/s
        /// <summary>
        /// Initializes a new instance of the <see cref="ObjectTextDumper" /> class with a text writer where to dump the object and initial indent 
        /// level.
        /// </summary>
        /// <param name="writer">The text writer where to dump the object to.</param>
        /// <param name="indentLevel">The initial indent level.</param>
        /// <exception cref="System.ArgumentNullException">
        /// Thrown if <paramref name="writer"/> is <see langword="null"/>.
        /// </exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2136:TransparencyAnnotationsShouldNotConflictFxCopRule"), SecuritySafeCritical]
        public ObjectTextDumper(
            TextWriter writer,
            int indentLevel = 0,
            int indentLength = 2)
        {
            if (writer == null)
                throw new ArgumentNullException("writer");

            _writer       = writer;
            _indentLevel  = indentLevel > 0 ? indentLevel : 0;
            _indentLength = indentLength > 0 ? indentLength : 2;
        }
        #endregion

        #region Public method/s
        /// <summary>
        /// Dumps the specified object in a text form to this object's <see cref="TextWriter"/> instance.
        /// </summary>
        /// <param name="value">
        /// The object to be dumped.
        /// </param>
        /// <param name="dumpMetadata">
        /// Optional metadata class to use to extract the dump parameters, options and settings. If not specified, the dump metadata will be
        /// extracted from the <see cref="MetadataTypeAttribute"/> attribute applied to <paramref name="value"/>'s class if specified otherwise from 
        /// the attributes applied within the class itself.
        /// </param>
        /// <param name="dumpAttribute">
        /// An explicit dump attribute to be applied at a class level. If not specified the <see cref="MetadataTypeAttribute"/> attribute applied to 
        /// <paramref name="value"/>'s class or <see cref="MetadataTypeAttribute.Default"/> will be assumed.
        /// </param>
        /// <returns>
        /// The <paramref name="value"/> parameter.
        /// </returns>
        [SuppressMessage("Microsoft.Security", "CA2136:TransparencyAnnotationsShouldNotConflictFxCopRule")]
        [SecuritySafeCritical]
        public ObjectTextDumper Dump(
            object value,
            Type dumpMetadata = null,
            DumpAttribute dumpAttribute = null)
        {
            var reflectionPermission = new ReflectionPermission(PermissionState.Unrestricted);
            var callerHasPermission = true;

            try
            {
                // make sure all callers on the stack have the permission to reflect on the object
                reflectionPermission.Demand();
            }
            catch (SecurityException)
            {
                callerHasPermission = false;
            }

            _writer.Indent(_indentLevel*_indentLength);

            try
            {
                if (!callerHasPermission)
                {
                    _writer.WriteLine();
                    // some caller does not have the permission - just call ToString() on the object.
                    _writer.Write(
                        Resources.CallerDoesNotHavePermissionFormat,
                        value!=null ? value.ToString() : DumpUtilities.Null);
                }
                else
                {
                    // assert the permission and dump
                    reflectionPermission.Assert();
#if DEBUG
                    var indentLevel = _indentLevel;
#endif
                    _maxDepth = null;

                    if (!DumpedBasicValue(value, dumpAttribute))
                    {
                        _writer.WriteLine();
                        DumpObject(value, dumpMetadata, dumpAttribute);
#if DEBUG
                        Debug.Assert(indentLevel == _indentLevel, "The indent level was not preserved.");
#endif
                    }
                }

                return this;
            }
            finally
            {
                // revert the permission assert
                if (callerHasPermission)
                    CodeAccessPermission.RevertAssert();

                // restore the original indent
                _writer.Unindent(_indentLevel*_indentLength);

                // clear the dumped objects register
                _dumpedObjects.Clear();
                // clear the dumped properties register
                _dumpedVirtualProperties.Clear();
            }
        }
        #endregion

        #region Private methods
        void Indent()
        {
            _writer.Indent(_indentLength);
        }

        void Unindent()
        {
            _writer.Unindent(_indentLength);
        }

        void DumpObject(
            object obj,
            Type dumpMetadata = null,
            DumpAttribute dumpAttribute = null)
        {
            if (DumpedBasicValue(obj, dumpAttribute))
                return;

            var type = obj.GetType();
            var classDumpData = dumpMetadata==null && dumpAttribute==null
                                        ? ClassMetadataResolver.GetClassDumpData(type)
                                        : new ClassDumpData(dumpMetadata ?? type, dumpAttribute);

            if (!_maxDepth.HasValue)
                _maxDepth = classDumpData.DumpAttribute.MaxDepth;

            if (_maxDepth < 0)
            {
                _writer.Write(Resources.DumpReachedMaxDepth);
                return;
            }

            _maxDepth--;
            using (var state = new DumpState(obj, type, classDumpData, dumpAttribute))
            {
                if (DumpedCollectionObject(state))
                    return;

                IEnumerable<DumpState> tailProperties = null;

                Indent();

                // dump recursively the top order and the bottom order properties:

                // dump all properties with non-negative order in class inheritance descending order (base classes' properties first)
                if (DumpTopProperties(state, ref tailProperties))
                    // dump all properties with negative order in class ascending order (derived classes' properties first)
                    DumpRemainingProperties(state, ref tailProperties);

                // dump all properties with Order=int.MinValue in ascending order (derived classes' properties first)
                DumpTailProperties(tailProperties);

                // if the object implements IEnumerable and the state allows it - dump the elements.
                DumpedCollectionObject(state, true);

                Unindent();
            }
            _maxDepth++;

            // this object is dumped already:
            _dumpedObjects.Add(new DumpedObject(obj, type));
        }

        /// <summary>
        /// Dumps the properties with non-negative dump order.
        /// </summary>
        /// <param name="state">The current dump state.</param>
        /// <returns><see langword="true"/> if all properties have been dumped; otherwise <see langword="false"/>.</returns>
        bool DumpTopProperties(
            DumpState state,
            ref IEnumerable<DumpState> bottomProperties)
        {
            if (IsAtDumpTreeLeaf(state))
                return false;

            var thisHasRemainingProperties = false;

            using (var baseDumpState = new DumpState(
                                                state.Instance,
                                                state.Type.BaseType,
                                                ClassMetadataResolver.GetClassDumpData(state.Type.BaseType),
                                                state.InstanceDumpAttribute))
            {
                // 1. dump the non-negative order properties of the base class
                var baseHasRemainingProperties = DumpTopProperties(baseDumpState, ref bottomProperties);

                // 2. dump the non-negative order properties of the current class
                while (state.MoveNext())
                {
                    if (state.CurrentPropertyDumpAttribute.Order < 0)
                    {
                        // if we reached a negative order property - set the flag that there are more to be dumped and suspend the dumping at this level
                        thisHasRemainingProperties = true;
                        break;
                    }

                    DumpProperty(state);
                }

                // 3. if the base class has more properties to be dumped - do it and pass the stack of dump states that have int.MinValue ordered properties
                if (baseHasRemainingProperties)
                    DumpRemainingProperties(baseDumpState, ref bottomProperties);
            }

            // we are all done here - return and tell them whether to call us again for #3.
            return thisHasRemainingProperties;
        }

        /// <summary>
        /// Dumps the properties with negative dump order.
        /// </summary>
        /// <param name="state">The state.</param>
        void DumpRemainingProperties(
            DumpState state,
            ref IEnumerable<DumpState> bottomProperties)
        {
            do
            {
                if (state.CurrentPropertyDumpAttribute.Order == int.MinValue)
                {
                    // we reached properties that should be dumped at the bottom of the dump - store the state in the stack and suspend the dump.
                    if (bottomProperties == null)
                        bottomProperties = new Stack<DumpState>();

                    ((Stack<DumpState>)bottomProperties).Push(state);
                    return;
                }

                DumpProperty(state);
            }
            while (state.MoveNext());
        }

        /// <summary>
        /// Dumps the properties with order int.MinValue
        /// </summary>
        /// <param name="tailProperties">The bottom properties collection (usually stack).</param>
        void DumpTailProperties(IEnumerable<DumpState> tailProperties)
        {
            if (tailProperties == null)
                return;

            foreach (var state in tailProperties)
                do
                    DumpProperty(state);
                while (state.MoveNext());
        }

        /// <summary>
        /// The dumping traverses depth-first a dump tree consisting of the object's properties its base classes' properties and the properties' 
        /// properties etc. This method determines if the recursion reached a leaf in the dump tree and the should stop drilling down and return to 
        /// dump other branches of the dump tree. Recursion stops when:
        /// <list type="bullet">
        /// <item>
        /// The current examined type is System.Object. The method dumps the <paramref name="obj"/>-s type name.
        /// </item>
        /// <item>
        /// The current examined base class has <see cref="DumpAttribute"/> with property <see cref="DumpAttribute.RecurseDump"/> set to <see cref="ShouldDump.Skip"/>. 
        /// If the attribute also defines the property <see cref="DumpAttribute.DefaultProperty"/> it will dump that property only as a representing
        /// property of the entire class.
        /// </item>
        /// <item>
        /// The examined object is a delegate. The method will dump the delegate type.
        /// </item>
        /// <item>
        /// The method determines that the current object has already been dumped (discovers circular reference). The method will dump a short 
        /// reference text.
        /// </item>
        /// </list>
        /// The current base class has class dump attribute with  examined is System.Object
        /// </summary>
        /// <param name="state">The current dump state.</param>
        /// <returns><see langword="true"/> if the recursion should stop; otherwise <see langword="false"/>.</returns>
        bool IsAtDumpTreeLeaf(DumpState state)
        {
            return DumpedAlready(state)                    ||     // the object has been dumped already (block circular and repeating references)
                   DumpedRootClass(state)                  ||     // stop the recursion at System.Object or at max depth
                   DumpedDefaultProperty(state)            ||     // stop if classDump.Recurse is false
                   DumpedDelegate(state)                   ||     // stop at delegates
                   DumpedMemberInfoValue(state.Instance);         // stop at MemberInfo
        }

        bool DumpedCollectionObject(
            DumpState state,
            bool enumerateCustom = false)
        {
            var sequence = state.Instance as IEnumerable;

            if (sequence == null)
                return false;

            return DumpedSequenceObject(
                        sequence,
                        state.ClassDumpData.DumpAttribute,
                        enumerateCustom);
        }

        void DumpProperty(
            DumpState state)
        {
            // should we dump it at all?
            if (!state.CurrentProperty.CanRead  ||  
                state.CurrentPropertyDumpAttribute.Skip == ShouldDump.Skip)
                return;

            var isVirtual = state.CurrentProperty.IsVirtual().GetValueOrDefault();

            if (isVirtual)
            {
                // don't dump virtual properties that were already dumped
                if (_dumpedVirtualProperties.Contains(new DumpedProperty(state.Instance, state.CurrentProperty.Name)))
                    return;

                // or will be dumped by the base classes
                if (state.CurrentPropertyDumpAttribute.IsDefaultAttribute() &&
                    PropertyDumpResolver.PropertyHasNonDefaultDumpAttribute(state.CurrentProperty))
                    return;
            }

            // can't dump indexers
            if (state.CurrentProperty.GetIndexParameters().Length > 0)
                return;

            object value = null;

            try
            {
                value = state.CurrentProperty.GetValue(state.Instance, null);
            }
            catch (TargetInvocationException)
            {
                // this should not happen but...
                value = null;
            }

            // should we dump a null value of the current property
            if (value == null  &&  
                (state.CurrentPropertyDumpAttribute.DumpNullValues==ShouldDump.Skip  ||
                 (state.CurrentPropertyDumpAttribute.DumpNullValues==ShouldDump.Default && 
                  state.DumpNullValues==ShouldDump.Skip)))
                return;

            if (isVirtual)
                _dumpedVirtualProperties.Add(new DumpedProperty(state.Instance, state.CurrentProperty.Name));

            // write the property header
            _writer.WriteLine();
            _writer.Write(
                state.CurrentPropertyDumpAttribute.LabelFormat,
                state.CurrentProperty.Name);

            if (value!=null && 
                state.CurrentPropertyDumpAttribute.ValueFormat == Resources.ValueFormatToString)
            {
                _writer.Write("{0}", value.ToString());
                return;
            }

            // dump primitive values, including string and DateTime
            if (DumpedBasicValue(value, state.CurrentPropertyDumpAttribute))
                return;

            if (DumpedDelegate(value))
                return;

            // dump a member which is reflection object
            if (DumpedMemberInfoValue(value))
                return;

            // dump sequences (array, collection, etc. IEnumerable)
            if (DumpedSequenceProperty(state, value))
                return;

            // dump a property representing an associated class or struct object
            DumpObject(
                value,
                null,
                state.CurrentPropertyDumpAttribute.IsDefaultAttribute() 
                    ? null
                    : state.CurrentPropertyDumpAttribute);
        }

        bool DumpedAlready(DumpState state)
        {
            var type = state.Instance.GetType();

            // stop the recursion at circular references
            if (!_dumpedObjects.Contains(new DumpedObject(state.Instance, type)))
                return false;

            _writer.Write(
                Resources.CyclicalReferenceDesignator,
                GetTypeName(type),
                type.Namespace,
                type.AssemblyQualifiedName);

            if (state.SetToDefault())
                DumpProperty(state);

            return true;
        }

        bool DumpedRootClass(DumpState state)
        {
            if (state.Type!=typeof(object))
                return false;

            var type = state.Instance.GetType();

            _writer.Write(
                Resources.TypeDumpFormat,
                GetTypeName(type),
                type.Namespace,
                type.AssemblyQualifiedName);

            return true;
        }

        bool DumpedDelegate(DumpState state)
        {
            if (!typeof(Delegate).IsAssignableFrom(state.Type))
                return false;

            // it will be dumped at the descendant level
            if (state.Type == typeof(MulticastDelegate) ||
                state.Type == typeof(Delegate))
                return true;

            return DumpedDelegate(state.Instance);
        }

        bool DumpedDelegate(object value)
        {
            var d = value as Delegate;

            if (d == null)
                return false;   // if it is null delegate, it will be dumped as basic value <null>

            _writer.Write(
                Resources.DelegateDumpFormat,
                d.Method.DeclaringType!=null ? d.Method.DeclaringType.Name : string.Empty,
                d.Method.DeclaringType!=null ? d.Method.DeclaringType.Namespace : string.Empty,
                d.Method.DeclaringType!=null ? d.Method.DeclaringType.AssemblyQualifiedName : string.Empty,
                d.Method.Name,
                d.Target==null 
                    ? Resources.ClassMethodDesignator 
                    : Resources.InstanceMethodDesignator);

            return true;
        }

        bool DumpedSequenceProperty(
            DumpState state,
            object value)
        {
            var sequence = value as IEnumerable;

            if (sequence == null)
                return false;

            return DumpedSequenceObject(sequence, state.CurrentPropertyDumpAttribute);
        }

        bool DumpedSequenceObject(
            IEnumerable sequence,
            DumpAttribute dumpAttribute,
            bool enumerateCustom = false)
        {
            Debug.Assert(sequence != null);

            if (!sequence.GetType().IsArray &&
                !Regex.IsMatch(sequence.GetType().Namespace, Resources.RegexSystemNamespace))
            {
                if (!enumerateCustom)
                    return false;

                if (dumpAttribute.Enumerate != ShouldDump.Dump)
                    return false;
                _writer.WriteLine();
            }

            var seqType = sequence.GetType();
            var collection = sequence as ICollection;
            var count = collection != null
                            ? collection.Count.ToString(CultureInfo.InvariantCulture)
                            :  string.Empty;
            var elementTypes = seqType.IsArray
                                    ? new Type[] { seqType.GetElementType() }
                                    : seqType.IsGenericType
                                        ? seqType.GetGenericArguments()
                                        : new Type[] { typeof(object) };

            _writer.Write(Resources.SequenceParamListBegin);
            for (var i = 0; i<elementTypes.Length; i++)
            {
                _writer.Write(
                    Resources.GenericParamFormat,
                    elementTypes[i].Name,
                    elementTypes[i].Namespace,
                    elementTypes[i].AssemblyQualifiedName);
                if (i < elementTypes.Length-1)
                    _writer.Write(Resources.GenericParamSeparator);
            }

            _writer.Write(
                Resources.SequenceParamListEnd,
                count);

            // how many items to dump max?
            var max = dumpAttribute.MaxLength;

            if (max < 0)
                max = int.MaxValue;
            else
                if (max == 0)        // limit sequences of primitive types (can be very big)
                    max = DumpAttribute.DefaultMaxElements;

            if (seqType == typeof(byte[]))
            {
                // dump no more than max elements from the sequence:
                var array = (byte[])sequence;
                var length = Math.Min(max, array.Length);

                _writer.Write(BitConverter.ToString(array, 0, length));
                if (length < array.Length)
                    _writer.Write(Resources.SequenceDumpTruncatedFormat, max);

                return true;
            }

            _writer.Write(
                    Resources.SequenceTypeFormat,
                    seqType.Name,
                    seqType.Namespace,
                    seqType.AssemblyQualifiedName);

            // stop the recursion if dump.Recurse is false
            if (dumpAttribute.RecurseDump==ShouldDump.Skip)
                return true;

            var n = 0;

            Indent();

            foreach (var item in sequence)
            {
                _writer.WriteLine();
                if (n++ < max)
                    DumpObject(item);
                else
                {
                    _writer.Write(Resources.SequenceDumpTruncatedFormat, max);
                    break;
                }
            }

            Unindent();

            return true;
        }

        bool DumpedDefaultProperty(
            DumpState state)
        {
            Debug.Assert(state.RecurseDump != ShouldDump.Default);

            if (state.RecurseDump == ShouldDump.Dump)
                return false;

            var type = state.Instance.GetType();

            _writer.Write(
                        Resources.TypeDumpFormat,
                        GetTypeName(type),
                        type.Namespace,
                        type.AssemblyQualifiedName);

            if (state.SetToDefault())
                DumpProperty(state);

            return true;
        }

        /// <summary>
        /// Gets the name of a type. In case the type is a EF dynamic proxy it will return only the first portion of the name, e.g.
        /// from the name "SomeTypeName_CFFF21E2EAC773F63711A0F93BE77F1CBC891DE8F0E5FFC46E7C4BB2E4BCC8D3" it will return only "SomeTypeName"
        /// </summary>
        /// <param name="type">The object which type name needs to be retrieved.</param>
        /// <returns>The type name.</returns>
        static string GetTypeName(Type type)
        {
            string typeName = type.Name;

            if (typeName.Length > 65 && 
                Regex.IsMatch(typeName.Substring(typeName.Length - 65-1), @"_[0-9A-F]{64}"))
                return type.BaseType.Name.Substring(0, typeName.Length-65);

            return typeName;
        }
        #endregion
    }
}
